# encoding: utf-8
#
# Redmine - project management software
# Copyright (C) 2006-2012  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

require 'redmine/scm/adapters/abstract_adapter'

module Redmine
  module Scm
    module Adapters
      class PlasticAdapter < AbstractAdapter

        class PlasticBranch < Branch 
          attr_accessor :is_default
        end

        class Revision < Redmine::Scm::Adapters::Revision
        end
        
        # Plastic executable name
        CM_BIN = Redmine::Configuration['scm_plastic_command'] || "cm"


        class << self
          def client_command
            @@bin    ||= CM_BIN
          end

          def sq_bin
            @@sq_bin ||= shell_quote_command
          end

          def client_version
            @@client_version ||= (scm_command_version || [])
          end

          def client_available
            !client_version.empty?
          end	

          def scm_command_version
            scm_version = scm_version_from_command_line.dup
            if scm_version.respond_to?(:force_encoding)
              scm_version.force_encoding('ASCII-8BIT')
            end
            #scm_version.is_a?(Array) ? scm_version.join('.') : scm_version.to_s.strip
            if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
              m[2].scan(%r{\d+}).collect(&:to_i)
            end            
          end

          def scm_version_from_command_line
            shellout("#{sq_bin} version") { |io| io.read }.to_s
          end
        end

        
        def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
          super
          logger.debug { "plastic initialize url:#{url}, root_url:#{root_url}" }
          #@root_url = root_url.blank? ? 'localhost:8087' : root_url
          @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
        end

        def path_encoding
          @path_encoding
        end

        def branches
          return @branches if @branches
          @branches = []
          delimiter = '_|_'
          cmd = %Q{#{self.class.sq_bin} find branch "on repository '#{url}'" --nototal --format="{name}#{delimiter}{id}#{delimiter}{changeset}"}
          default = default_branch
          logger.debug { "<plastic> branches default:[#{default}] " }
          shellout(cmd) do |io|
            io.each_line do |line|
              name, id, changeset = line.split(delimiter)
              logger.debug { "branches: name:[#{name}], id:[#{id}], changeset:[#{changeset}]" }
              bran = PlasticBranch.new(name)
              bran.revision = changeset.chomp
              bran.scmid = changeset.chomp
              bran.is_default = name == default
                @branches << bran                
            end
          end
          @branches.sort!
        rescue ScmCommandAborted
          nil
        end

        def default_branch
          "/main"
        end

        def tags
          return @tags if @tags
          cmd = %Q{#{self.class.sq_bin} find label "on repository '#{url}'" --nototal --format="{name}"}
          shellout(cmd) do |io|
            @tags = io.readlines.sort!.map{|t| t.strip}
          end
        rescue ScmCommandAborted
          nil
        end

        #
        # cm ann support only current active branch  (= home) path
        #
        #def annotate(path, identifier=nil)
        #  
        #  return nil if root_url.nil? || root_url.empty?
        #  cmd_args = %w|annotate|
        #  cmd_args << path
        #  blame =  Annotate.new
        #  scm_cmd(cmd_args) { |io| io.binmode; content = io.read }
        #  blame
        #rescue ScmCommandAborted
        #  nil          
        #end

        # Returns the entry identified by path and revision identifier
        # or nil if entry doesn't exist in the repository
        #
        # add options = {:report_last_commit => false} when call entries
        def entry(path=nil, identifier=nil)
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
          search_path = parts[0..-2].join('/')
          search_name = parts[-1]
          if search_path.blank? && search_name.blank?
            # Root entry
            Entry.new(:path => '', :kind => 'dir')
          else
            # Search for the entry in the parent directory
            es = entries(search_path, identifier,
                         options = {:report_last_commit => false})
            es ? es.detect {|e| e.name == search_name} : nil
          end
        end
        
        # Returns an Entries collection
        # or nil if the given path doesn't exist in the repository
        #
        # default format string is: (cm ls)
        # "{<size>,10} {<date>:dd/MM/yyyy} {<date>:HH:mm}
        #  {<type>,-6} {<location>,-12} {<co>,-5} {name}
        #  {symlinktarget}"
        def entries(path=nil, identifier=nil, options={})
          logger.debug  { "<plastic> entries: path:#{path}, identifier:#{identifier}" }
          logger.debug  { "<plastic> entries: options:#{options}" }
          path ||= ''
          p = scm_iconv(@path_encoding, 'UTF-8', path)          
          repo_server = url
          branch_name = identifier.nil? ? default_branch : identifier
          cmd_args = %w|ls|
          cmd_args << "/#{p}"
          cmd_args << "--selector=\"rep '#{repo_server}' path '/' branch '#{branch_name}'\""
          cmd_args << '--format="{<size>,10}@{<date>:yyyy/MM/dd HH:mm:ss}@{type}@{changeset}@{<location>,-12}@{owner}@{name}"'
          entries = Entries.new
          return entries if root_url.nil? || root_url.empty?
          scm_cmd(cmd_args) do |io|
            io.each_line do |line|
              e = line.chomp.to_s.split('@')
              logger.debug  { "entries: e.size:#{e.size}, #{e}" }
              # 4 depends on format
              if e.size == 7
                size = e[0]
                date = e[1]
                type = e[2].strip
                changeset = e[3]
                lastrev = e[4].split('#')[1].strip
                owner = e[5]
                name = e[6]
                if name.respond_to?(:force_encoding)
                  name.force_encoding(@path_encoding)
                end
                full_path = p.empty? ? name : "#{p}/#{name}"
                n      = scm_iconv('UTF-8', @path_encoding, name)
                kind = (type == "dir") ? 'dir' : (type == "txt") ? 'text' : 'file'
                next if (type == "dir") and (name == ".")
                entries << Entry.new({:name => n,
                  :path => full_path,
                  :kind => kind,
                  :size => (type == "dir") ? nil : size,
                  :lastrev => Revision.new({:identifier => changeset, :time => Time.parse(date), :author => owner })
                  }) unless entries.detect {|entry| entry.name == name}
              end
            end
          end
          entries.sort_by_name
        rescue ScmCommandAborted
          nil
        end

        #
        #
        #
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
          logger.debug {"<plastic> revisions path=#{path}, from=#{identifier_from}, to=#{identifier_to}, options=#{options.inspect}"}
          revs = Revisions.new
          brs = get_branches_on_revisions(identifier_from, identifier_to, options)          
          # brs --> changeset --> revisions, moved, removed, 
          brs.each do |br,value|
            cmd = get_cmd_find_changeset(br, value)
            cm_find(cmd) do |io|
              output = io.read
	          if output.respond_to?(:force_encoding)
	              output.force_encoding('UTF-8')
	          end
	          begin
                doc = parse_xml(output)
                each_xml_element(doc['PLASTICQUERY'], 'CHANGESET') do |changeset|
                  #logger.debug "<plastic> revisions changeset:#{changeset}"
                  changesetid = changeset['CHANGESETID']['__content__'] #changeset.elements['CHANGESETID'].text
                  files = update_files(changesetid)
                  #logger.debug "<plastic> revisions file cs:#{changesetid} ... files:#{files.inspect}"
                  attributes = get_attribute_on_revision(changeset, changesetid, files)
                  revision = Revision.new(attributes)
                  if block_given?
                    yield revision
                  else
                    revs << revision
                  end
                end
              end
            end
          end
          revs
        end

        def get_cmd_find_changeset(br, value)
          cmd = %w|changeset|
          cmd <<  %Q{where "branch='#{br.to_s}'"}            
          #cmd = %Q{#{self.class.sq_bin} find changeset where branch=#{br.to_s} "on repository '#{url}'"}
          unless value.nil?
            unless value[:rev_to].nil?
              cmd << "and"
              cmd << %Q{"changesetid <= #{value[:rev_to]}"}
            end
            unless value[:rev_from].nil?
              cmd << "and"
              cmd << %Q{"changesetid > #{value[:rev_from]}"}
            end
          end
          cmd << %Q{"on repository '#{url}'"}
          cmd << %Q{--dateformat="yyyy-MM-dd HH:mm:ss"}
          cmd << "--xml"
          cmd
        end

        def get_branches_on_revisions(identifier_from=nil, identifier_to=nil, options={})
          brs ||= {}
          if identifier_from || identifier_to
            if identifier_from
              logger.debug { "<plastic> brs identifier_from:#{identifier_from}" }
              brs[identifier_from.to_sym] = {}
            end
            if identifier_to
              logger.debug { "brs identifier_to:#{identifier_to}" }
              brs[identifier_to.to_sym] = {}
            end
          else
            #scm_brs = branches
            #scm_brs.each_with_index do |br,i|
            #br_hash = { :name => br }
            unless (options[:includes].nil? || options[:includes].empty?) #.blank?
              include_brs = branches_with_key(options[:includes],:rev_to)
              logger.debug "brs #{include_brs}"
              brs.merge!(include_brs)
            end
            unless options[:excludes].nil? || options[:excludes].empty? #.blank?
              excludes_brs = branches_with_key(options[:excludes], :rev_from)
              logger.debug "brs #{include_brs}"
              brs.merge!(include_brs)
            end            
          end
          brs
        end
        # array : changset array
        def branches_with_key(array, key, h = nil)
          brs = h || {}
          delimiter = '_|_'
          array.each do |scmid| 
            cmd = %Q{#{self.class.sq_bin} find changeset where "changesetid=#{scmid}" "on repository '#{url}'" --nototal --format="{branch}#{delimiter}{changesetid}"}
            shellout(cmd) do |io|
            io.each_line do |line|
              name, changeset = line.split(delimiter)
              name_to_sym = name.to_sym
              logger.debug { "<plastic> branches_with_key name:#{name}, changeset:#{changeset}" }
              unless brs.has_key?(name_to_sym)
                brs [name_to_sym]={}
              end
              brs [name_to_sym][key]=changeset.chomp
              end
            end
          end
          return brs
        rescue ScmCommandAborted
          nil          
        end

        def update_files(csid, files = nil)
          result = files || []
          result = update_files_for_revision(csid, result)
          result = update_files_for_moved(csid, result) unless root_url.nil?                    
          result = update_files_for_removed(csid, result) unless root_url.nil?
          result
        end

        def update_files_for_revision(csid, files = nil)
          result = files || []
          ## find revision
          #cmd_revs = %w|revision|
          #cmd_revs << "where changeset=#{csid}"
          #cmd_revs << %Q{"on repository '#{url}'"}
          #cmd_revs << "--xml"
          cmd_revs = %Q{#{self.class.sq_bin} find branch where changeset=#{csid} "on repository '#{url}'" --xml}
          shellout(cmd_revs) do |io|
            output = io.read
            if output.respond_to?(:force_encoding)
              output.force_encoding('UTF-8')
	          end
            begin
              doc = parse_xml(output)
              each_xml_element(doc['PLASTICQUERY'], 'REVISION') do |rev|
                filepath = rev['PATH']['__content__']
                fileaction = rev['PARENT']['__content__'] == "-1" ? "A" : "C"
                logger.debug { "<plastic> update_files_for_revision ... action:#{fileaction}, path:#{filepath}" }
                #logger.debug { "<plastic> update_files_for_revision ... rev:#{rev}" }
                # rev.elements['PATH'].text
                #(rev.elements['PARENT'].text == "-1") ? "A" : "C"
                result << {:action=>fileaction, :path=> filepath}
              end
            rescue
            end
          end
          result          
        #rescue ScmCommandAborted => e
        #  err_msg = "cm log error: #{e.message}"
        #  logger.error(err_msg)
        #  if block_given?
        #    raise CommandFailed, err_msg
        #  else
        #    result
        #  end
        end

        def update_files_for_moved(csid, files = nil)
          result = files || []
          result
        end
        def update_files_for_removed(csid, files = nil)
          result = files || []
          result
        end
        def get_attribute_on_revision(changeset, changesetid, files)
          parents = [ changeset['PARENT']['__content__'] ]
          attribute = {
            :identifier => changesetid,
            :scmid => changesetid, #changeset['ID']['__content__'],            #changeset.elements['ID'].text,
            :author => changeset['OWNER']['__content__'],           #changeset.elements['OWNER'].text,
            :time => Time.parse(changeset['DATE']['__content__']), #Time.parse(changeset['DATE'].text),
            :message => changeset['COMMENT']['__content__'],     #changeset.elements['COMMENT'].text,
            :paths => files,
            :branch => changeset['BRANCH']['__content__'],       #changeset.elements['BRANCH'].text,
            :parents => parents                                  #changeset.elements['PARENT'].text.to_a
          }
        end



        def lastrev(path, rev)
          return nil if path.brank?
          cmd_args =%w|ls|
          cmd_args << path
          lines = []
          scm_cmd(cmd_args) { |io| lines = io.readlines }
          begin
              id = lines[0].split[1]
              author = lines[1].match('Author:\s+(.*)$')[1]
              time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])

              Revision.new({
                :identifier => id,
                :scmid      => id,
                :author     => author,
                :time       => time,
                :message    => nil,
                :paths      => nil
                })
          rescue NoMethodError => e
              logger.error("The revision '#{path}' has a wrong format")
              return nil
          end
        rescue ScmCommandAborted
          nil
        end

        # 
        def diff(path, rev, rev_to=nil)
          if root_url.blank?
            raise ScmCommandAborted.new("Workspace on disk cann't blank for diff... Check the settings for the PlasticScm Repository(admin only)")
          end
          logger.info {"plastic diff path:#{path} rev:#{rev} rev_to:#{rev_to}" }
          path ||= ''
          cmd_args = %w|diff|
          if rev_to
            
            cmd_args << %Q|"cs:#{rev_to}"|
            cmd_args << %Q|"cs:#{rev}"|
          else
            cmd_args << %Q|"cs:#{rev}"|
          end
          diff = []
          scm_cmd(cmd_args) do |io|
            io.each_line do |line|
              logger.info { "plastic diff line: #{line}" }
              diff << line
            end
          end
          logger.info { "plastic diff: #{diff}" }
          diff
        rescue ScmCommandAborted => e
          err_msg = "cm diff error: #{e.message}"
          raise CommandFailed.new(err_msg)
        end

        # not yet support (needs workspace or cm command )
        # 
        def cat(path, identifier=nil)
          logger.debug {"plastic cat path:#{path} id:#{identifier} " }
          return nil
        end        

        private

        def cm_find(args, options = {}, &block)
          cm_command("find", args, options, &block)
        end
        
        def cm_command(command, args, options = {}, &block)
          full_args = []
          full_args += args
          ret = shellout(
                   self.class.sq_bin + " #{command} " + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          if $? && $?.exitstatus != 0
            raise ScmCommandAborted, "cm exited with non-zero status: #{$?.exitstatus}"
          end
          ret
        end
        #
        #
        #
        def revisions222(path=nil, identifier_from=nil, identifier_to=nil, options={})
          logger.debug {"<plastic> revisions path=#{path}, from=#{identifier_from}, to=#{identifier_to}, options=#{options}"}
          revs = Revisions.new
          # setup cmd_args
          path_to_workspace = url
          repository_server = root_url
          brs ||= []
          if identifier_from || identifier_to
            each_br = {}
            if identifier_to
              each_br[:name] = identifier_to
            end
            if identifier_from
              each_br[:rev_from] = identifier_from
            end
            brs << each_br
          else
            scm_brs = branches
            scm_brs.each_index do |x|
            	each_br = { :name => scm_brs[x] }
            	unless options[:includes].blank?
              	each_br[:rev_to] = options[:includes][x]
              	
            	end
            	unless options[:excludes].blank?
              	each_br[:rev_from] = options[:excludes][x]
            	end
            	logger.debug { "<plastic> revisions ... each_br:#{each_br}"}
            	brs << each_br
            end
          end
          logger.debug { "<plastic> revisions ... brs:#{brs}"}          

          brs.each do |br|
            logger.debug { "<plastic> revisions .... br:#{br}" }
	          cmd_args =%w|find changeset where|
	          cmd_args << %Q|"branch=\'#{br[:name]}\'"|
	          cmd_args << "and \"changesetid <= #{br[:rev_to]}\"" unless br[:rev_to].nil?
	          cmd_args << "and \"changesetid > #{br[:rev_from]}\"" unless br[:rev_from].nil?
	          cmd_args << %Q|"on repository \'#{repository_server}\'"|
	          cmd_args << "--xml"
	          logger.debug { "<plastic> revisions cmd_args:#{cmd_args}" }
	          scm_cmd(cmd_args) do |io|
	            output = io.read
	            if output.respond_to?(:force_encoding)
	              output.force_encoding('UTF-8')
	            end
	            begin
	              files = []
	              doc = parse_xml(output)
	              logger.debug { "doc:#{doc}" }
	              each_xml_element(doc['PLASTICQUERY'], 'CHANGESET') do |changeset|
	                changeset_name = changeset['CHANGESETID']['__content__']
	                # single log for each changeset
	                log_cmd_args = %w|log|
	                log_cmd_args << "cs:#{changeset_name}"
	                scm_cmd(log_cmd_args) do |log_io|
	                  log_io.each_line do |log_line|
	                    if m = log_line.match(%r{^\s([ACMD])\s(.+)\s+.?(\d+)$})
	                      logger.debug { "<plastic> revisions log_line: matched#{m[0]}" }
	                      logger.debug { "<plastic> revisions log_line: m[1]:#{m[1]}" }
	                      logger.debug { "<plastic> revisions log_line: m[2]:#{m[2]}" }
	                      logger.debug { "<plastic> revisions log_line: m[3]:#{m[3]}" }
				                fileaction = m[1]
	      			          filepath = m[2]
	                 	    p = scm_iconv('UTF-8', @path_encoding, filepath)                      
	                      files << {:action => fileaction, :path => p}
	                    end
	                  end
	                end
	                parents = [ changeset['PARENT']['__content__'] ]
	                attributes={
	                  :identifier => changeset['CHANGESETID']['__content__'],
	                  :scmid => changeset['ID']['__content__'],
	                  :name => changeset_name,
	                  :author => changeset['OWNER']['__content__'],
	                  :time => Time.parse(changeset['DATE']['__content__']),
	                  :message => changeset['COMMENT']['__content__'],
	                  :paths => files,
	                  :branch => changeset['BRANCH']['__content__'],
	                  :parents => parents
	                }
	                # logger.debug { "attribues: => #{attributes}" }
	                # :paths is not yet initialized
	                revision = Revision.new(attributes)
	                # logger.debug { "revision => #{revision}" }
	                if block_given?
		                yield revision
	    	          else
	        	        revs << revision
	                end
	              end
	            rescue
	            end
	          end
	        end
          revs
        rescue ScmCommandAborted => e
          err_msg = "plastic revisions error: #{e.message}"
          logger.error(err_msg)
          if block_given?
            raise CommandFailed, err_msg
          else
            revs
          end
        end        
        
			  # Returns the relative url of the repository
			  # Eg: root_url = file:///var/svn/foo
			  #     url      = file:///var/svn/foo/bar
			  #     => returns /bar
			  def relative_url
			    @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(@root_url)}", Regexp::IGNORECASE), '')
			  end
			  
        def scm_cmd(args, options = {}, &block)
          cd_path_to_workspace = %w|cd|
          cd_path_to_workspace << "/D" if Redmine::Platform.mswin?
          cd_path_to_workspace << root_url
          cd_path_to_workspace << "&&"
          full_args = []
          full_args += args
          ret = shellout(
          				 cd_path_to_workspace.map { |e| e.to_s }.join(' ') + ' ' +
                   self.class.sq_bin + ' ' + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          if $? && $?.exitstatus != 0
            raise ScmCommandAborted, "plastic exited with non-zero status: #{$?.exitstatus}"
          end
          ret
        end
        
        def scm_cmd_no_rais(args, options = {}, &block)
          full_args = []
          full_args += args
          ret = shellout(
                   self.class.sq_bin + ' ' + full_args.map { |e| e.to_s }.join(' '),
                   options,
                   &block
                   )
          ret
        end

        # Helper that iterates over the child elements of a xml node
        # MiniXml returns a hash when a single child is found
        # or an array of hashes for multiple children
        def each_xml_element(node, name)
          if node && node[name]
            if node[name].is_a?(Hash)
              yield node[name]
            else
              node[name].each do |element|
                yield element
              end
            end
          end
        end
        
      end
    end
  end
end
